﻿#region copyright
//<copyright>
// Copyright(C) 2012 TrackerRealm Corporation
// This file is part of the open source project - Jazz. http://jazz.codeplex.com
// 
// Jazz is open software: you can redistribute it and/or modify it 
// under the terms of the GNU Affero General Public License (AGPL) as published by 
// the Free Software Foundation, version 3 of the License.
// 
// Jazz is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied warranty 
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
//  See the GNU Affero General Public License (AGPL) for more details.
// 
// You should have received a copy of the GNU General Public 
// License along with Jazz.  If not, see <http://www.gnu.org/licenses/>.
//
// REMOVAL OF THIS NOTICE IS VIOLATION OF THE COPYRIGHT. 
//</copyright>
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using TrackerRealm.Jazz.Common;
using C = TrackerRealm.Jazz.Common.JazzConstants;
using System.Runtime.Serialization;
using TrackerRealm.Jazz.Client.Services;

namespace TrackerRealm.Jazz.Client
{
    /// <summary>
    /// Client Nexus
    /// </summary>
    public class ClientNexusBase: IDisposable
    {
        //private bool isClientNexusStarted = false;
        #region Constructors
        //public class Nexus<U,P> where U: jBaseUser where P: jProfileBase
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="nexusConfig"></param>
        internal protected ClientNexusBase(NexusConfigBase nexusConfig)
        {
            this.cache = new Cache(this);
            this.Verbose = nexusConfig.Verbose;
            this.IsVoidSafetyEnhanced = nexusConfig.VoidSafetyEnhanced;
            this.RoleCheckCollection = nexusConfig.RoleCheckCollection;

            List<jObject> emptyList = new List<jObject>(16);
            List<jUserBase> userList = new List<jUserBase>(8);

            List<jProfileBase> profileList = new List<jProfileBase>(8);
            List<jProfileBase> emptyProfiles = new List<jProfileBase>(8);
            List<jWorkspaceBase> wsList = new List<jWorkspaceBase>(8);
            List<jUserBase> emptyUsers = new List<jUserBase>(8);
            List<jRoleBase> roleList = new List<jRoleBase>(8);
            List<jRoleBase> emptyRoles = new List<jRoleBase>(8);
            jWorkspaceBase publicWs = null;
            List<jWorkspaceBase> emptyWsList = new List<jWorkspaceBase>(8);
            jObjectEmpty emptyjObj = null;
            //
            // parse the 'BindUnits' looking for specific types of 'jObject' classes
            //
            jObject[] jazzObjs = nexusConfig.BindUnits.Where(u => u is jObject).Cast<jObject>().ToArray();
            nexusConfig.BindUnits.Where(u => u is jObject).Cast<jObject>().Foreach(j => 
            {
                // need error message if not clientNexus null
                j.Flags = j.Flags.Reset(JazzFlagsType.Bound);
                if (j.IsEmpty && !(j is jObjectEmpty)) emptyList.Add(j);
                if (j is jUserBase) 
                {
                    userList.Add((jUserBase)j);
                    // there may be more than 1 empty jUser
                    if (j.IsEmpty) emptyUsers.Add((jUserBase)j);
                }
                else if (j is jWorkspaceBase)
                {
                    wsList.Add((jWorkspaceBase)j);
                    if (j.Name == C.WORKSPACE_PUBLIC)
                        publicWs = (jWorkspaceBase)j;
                    if (j.IsEmpty) emptyWsList.Add((jWorkspaceBase)j);  // there may be more than 1 empty jWorkspace
                }
                else if (j is jRoleBase)
                {
                    jRoleBase role = (jRoleBase)j;
                    roleList.Add(role);
                    if (j.IsEmpty) emptyRoles.Add(role);  // there may be more than 1 empty jRole
                }
                else if (j is jProfileBase)
                {
                    jProfileBase profile = (jProfileBase)j;
                    profileList.Add(profile);
                    if (j.IsEmpty) emptyProfiles.Add(profile);  // there may be more than 1 empty jProfile
                }
                else if (j is jObjectEmpty)
                {
                    emptyjObj = (jObjectEmpty)j;
                }
            });
            //
            // Bind the public workspace
            //
            emptyWsList.ForEach(ws => Bind(ws.GetType(), ws));
            if (publicWs != null)
                this._PublicWorkspace = publicWs;
            if (this._PublicWorkspace == null)
            {
                Tuple<jWorkspaceBase, jWorkspaceBase> wsTuple = nexusConfig._PublicWorkspace;
                this._PublicWorkspace = wsTuple.Item1;
                this.Bind(wsTuple.Item2.GetType(), wsTuple.Item2);
                if (wsTuple.Item2 != null)
                    this.Bind(wsTuple.Item2);  // empty Workspace
            }
            this.Bind(this._PublicWorkspace);

            // Create jObject empty
            //
            if(emptyjObj != null)
                this.BindNoCheck(typeof(jObject), emptyjObj, null, null);
            else this.Bind(typeof(jObject));
            //
            // Assign the Login User - choose the from either the "LoginProfile" or "BindUnits"
            //
            #region Assign the Login User
            // Basic start scenerio:
            // 1.  On first use 'BindUnits' will be empty, therefore use the 'LoginProfile',
            //     which will contain a freshly created 'Guest'.
            // 2.  Save the cache.
            // 3.  Restore the saved cached via 'BindUnits'.  
            //     This time the 'Guest' from 'BindUnits' will be used instead of the freshly created 'LoginProfile' - 'Guest'.
            string loginName = nexusConfig._LoginProfile.Name;
            if (userList.FirstOrDefault(u => u.Name == loginName) == null  &&
                (profileList.FirstOrDefault(u => u.Name == loginName) == null))
            {
                //use login property because no user or profile was found in 'BindUnits' with the same name as the 'LoginProfile' property.
                this._LoginRoles = nexusConfig._LoginProfile._Roles;
                this._LoginProfile = nexusConfig._LoginProfile;
            }
            else
            {
                // use the login profile found in the "BindUnits" that has the same name as the property 'LoginProfile'
                jUserBase userCandidate = userList.Where(u => u.Name == loginName).FirstOrDefault();
                if (userCandidate != null)
                {
                    if (userCandidate._Profile == null)
                        throw new InvalidOperationException("The login user has a null profile");
                    this._LoginProfile = userCandidate._Profile;
                }
                else
                {
                    this._LoginProfile = profileList.First(u => u.Name == loginName);
                }
                this._LoginRoles = this._LoginProfile._Roles;
            }
            if (this._LoginProfile._User == null)
                throw new InvalidOperationException("The login profile has a null user");
            #endregion
            // Bind Roles
            emptyRoles.ForEach(r => Bind(r.GetType(), r));
            roleList.ForEach(r => Bind(r));
            this.BindIEnumerable(this._LoginRoles); 
            //
            // bind Login User
            emptyUsers.Foreach(u => Bind(u.GetType(), u));
            this.Bind(this._LoginProfile._User);
            //
            // Bind Login profile
            emptyProfiles.ForEach(p => Bind(p.GetType(), p));
            //this.Bind(typeof(jProfileBase), ((jUserBase)this.LoginProfile._User.Class.EmptyInstance)._Profile);
            this.Bind(this._LoginProfile.GetType(), ((jUserBase)this._LoginProfile._User.Class.EmptyInstance)._Profile);
            this.Bind(this._LoginProfile);
            //
            // bind storage objects.
            // 
            if (emptyList.FirstOrDefault(e => e._ACL.Count() > 0) != null)
                throw new InvalidOperationException("Empty objects must have an empty ACL.");
            if (emptyList.FirstOrDefault(e => e.Workspace != this._PublicWorkspace) != null)
                throw new InvalidOperationException("Empty objects must be in the 'Public' workspace.");
            emptyList.ForEach(j => this.Bind(j.GetType(), j));
            foreach (IjStorageUnit u in nexusConfig.BindUnits)
            {
                if (!(u is jObject))
                {
                    throw new ArgumentException("Can only accept 'jObject's in the 'StorageObjects' property.");
                }
                this.Bind((jObject)u);
            }
            //// bind the login roles if they have not been bound
            //foreach (jRoleBase r in this._LoginRoles)
            //{
            //    if (!r.Flags.HasFlag(JazzFlagsType.Bound))
            //        this.Bind(r);
            //}
            this.SetObjectAccessibility();
            //isClientNexusStarted = true;
        }
        #endregion
        //
        // Instance properties
        //
        #region IsVoidSafetyEnhanced
        /// <summary>
        /// <para>*** Not fully Implemented ***</para>
        /// When true empty Jazz objects are used instead of null to reduce
        /// the chances of getting 'NullReferenceException', 'ArgumentNullException', etc.
        /// </summary>
        public bool IsVoidSafetyEnhanced
        {
            get;
            private set;
        }
        #endregion
        #region Verbose
        /// <summary>
        /// 
        /// </summary>
        public VerboseType Verbose
        {
            get;
            private set;
        }
        #endregion

        #region RoleCheckCollection
        /// <summary>
        /// 
        /// </summary>
        public IEnumerable<string> RoleCheckCollection
        {
            get;
            private set;
        }
        #endregion
        #region LoginRoles
        /// <summary>
        /// 
        /// </summary>
        internal protected IEnumerable<jRoleBase> _LoginRoles
        {
            get;
            private set;
        }
        #endregion
        #region LoginProfile
        /// <summary>
        /// 
        /// </summary>
        internal protected jProfileBase _LoginProfile
        {
            get;
            private set;
        }
        #endregion
        #region Cache
        private Cache cache;
        /// <summary>
        /// A cache containing all objects that have been bound (see 'Bind' method)
        /// to the client nexus.
        /// </summary>
        public Cache Cache
        {
            get { return this.cache; }
        }
        #endregion
        #region PublicWorkspace
        /// <summary>
        /// 
        /// </summary>
        internal protected jWorkspaceBase _PublicWorkspace
        {
            get;
            private set;
        }
        #endregion
        //
        // Instance Methods
        #region Bind
        /// <summary>
        /// Binds a workflows to this client nexus. 
        /// <para>A workflow needs to be bound when first created or restored.</para>
        /// <para>Workflow may only be bound to a single nexus only once and may not be rebound to any other client nexus.</para>
        /// </summary>
        /// <param name="jazzObjects"></param>
        public void BindIEnumerable(IEnumerable<jObject> jazzObjects)
        {
            foreach (jObject wf in jazzObjects)
            {
                this.Bind(wf);
            }
        }
        /// <summary>
        /// Binds a workflows to this client nexus. 
        /// <para>A workflow needs to be bound when first created or restored.</para>
        /// <para>Workflow may only be bound to a single nexus only once and may not be rebound to any other client nexus.</para>
        /// </summary>
        /// <param name="jazzObjects"></param>
        public void Bind(params jObject[] jazzObjects)
        {
            this.BindIEnumerable(jazzObjects);
        }

        /// <summary>
        /// Binds a workflow to this client nexus. 
        /// <para>A workflow needs to be bound when first created or restored.</para>
        /// <para>Workflow may only be bound to a single nexus only once and may not be rebound to any other client nexus.</para>
        /// </summary>
        /// <param name="jazzObj"></param>
        public void Bind(jObject jazzObj)
        {
            if (jazzObj.IsBound)
            {
                if (jazzObj.ClientNexus == this) return;
                throw new ArgumentException("Bind operation - object is already bound to another client nexus.", "jazzObj");
            }
            BindObject(jazzObj, jazzObj.GetType());
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="jazzObj"></param>
        /// <param name="jObjectType"></param>
        private void BindObject(jObject jazzObj, Type jObjectType)
        {
            // check if class is already bound
            jClass jC;
            if (!this.Cache.JClasses.TryGet(jObjectType, out jC))
            {
                this.cache.JClasses.AddPlaceHolder(jObjectType, jClass.Placeholder);
                jC = Bind(jObjectType);
            }
            if (jC == jClass.Placeholder) return; //

            jClass.jObject_JazzClassField.SetValue(jazzObj, jC);
            if (jazzObj.Workspace == null)
                jazzObj.Workspace = this._PublicWorkspace;

            jClass.jObject_ClientNexusField.SetValue(jazzObj, this);
            if (!jazzObj.Flags.HasFlag(JazzFlagsType.Initialized))
            {
                // Set Jazz.ID
                jClass.jObject_IdField.SetValue(jazzObj, jazzObj.OnAssignID());
                // set initial values
                jC.InitializeJObjectFields(jazzObj);
                jazzObj.OnInitialBind();
            }
            jazzObj.OnBind();
            if (jazzObj.IsEmpty && this._PublicWorkspace!= null && jazzObj.Workspace != this._PublicWorkspace)
                throw new InvalidOperationException(string.Format("Empty objects must be in the 'Public' workspace. {0}", jazzObj.FullName));
            jazzObj.Flags= jazzObj.Flags.Set(JazzFlagsType.Initialized |JazzFlagsType.Bound);

            if (!this.Cache.Contains(jazzObj.ID)) 
                this.Cache.Add(jazzObj);
            jazzObj.IsAccessibilitySet = false;
            jazzObj.SetAccessibility(this);
        }
        /// <summary>
        /// Create a jClass and a 'Empty' jObject for the parameter '<paramref name="jazzObjectType"/>'.
        /// </summary>
        /// <param name="jazzObjectType"></param>
        /// <returns></returns>
        public jClass Bind(Type jazzObjectType)
        {
            jClass returnClass = null;
            if (this.cache.JClasses.TryGet(jazzObjectType, out returnClass) && returnClass != jClass.Placeholder)
                return returnClass;
            returnClass = null;
            Type t = jazzObjectType;
            jObject emptyWf = null;
            ConstructorInfo ctor = null;
            object[] ctorParams = null;
            string className = null;
            bool isBindNoCheck = false;
            if(t.IsAbstract && !t.TryGetEmptyObjectAttribute(out className))
            {
                throw new ArgumentException(string.Format("Can not bind type '{0}'because it is abstract.  Use the attribute 'EmptyObject' or 'UseEmptyObjectFor' to eliminate this error.", t.FullName));
            }
            if (t.IsAbstract)
            {
                isBindNoCheck = true;
                Type jazzClassType = null;
                jClass jazzClass = this.Cache.JClasses.Where(jC => jC.Name == className).FirstOrDefault();
                if (jazzClass != null) // class already in cache
                    emptyWf = jazzClass.EmptyInstance;
                else 
                {
                    jazzClassType = JazzServices.GetTypeFromShortName(className);

                    if (jazzClassType.IsEmptySingletonAttribute())
                    {
                        emptyWf = (jObject)FormatterServices.GetUninitializedObject(jazzClassType);
                        ctor = jazzClassType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, new Type[0], new ParameterModifier[0]);
                        if (ctor == null)
                            throw new ApplicationException("Expecting default constructor");
                        ctorParams = new object[0];
                    }
                    else
                        emptyWf = this.Empty(jazzClassType);
                }
            }
            else
            {
                MethodInfo[] ms = t.GetMethods();
                MethodInfo m = t.GetMethod("CreateEmpty", BindingFlags.Static | BindingFlags.Public, null, new Type[0], new ParameterModifier[0]);
                if (m != null)
                {
                    emptyWf = (jObject)m.Invoke(null, new object[0]);
                }
                else
                {
                    m = t.GetMethod("CreateEmpty", BindingFlags.Static | BindingFlags.Public, null, new Type[] { this.GetType() }, new ParameterModifier[0]);
                    if (m != null)
                    {
                        emptyWf = (jObject)m.Invoke(null, new object[] { this });
                        if (emptyWf.Flags.HasFlag(JazzFlagsType.Bound))
                            throw new ApplicationException("Do not 'Bind' in the 'CreateEmpty' method.");
                    }
                }
                if (emptyWf == null)
                {
                    emptyWf = (jObject)FormatterServices.GetUninitializedObject(t);
                    ctor = t.GetConstructor(new Type[0]);
                    if (ctor != null)
                        ctorParams = new object[0];
                    if (ctor == null)
                    {
                        ctor = t.GetConstructor(new Type[] { typeof(string) });
                        ctorParams = new object[] { C.EMPTY };
                    }
                    if (ctor == null)
                    {
                        ctor = t.GetConstructor(new Type[] { this.GetType() });
                        ctorParams = new object[] { this };
                    }
                    if (ctor == null)
                    {
                        ctor = t.GetConstructor(new Type[] { this.GetType(), typeof(string) });
                        ctorParams = new object[] { this, C.EMPTY };
                    }
                    if (ctor == null)
                    {
                        if (this.IsVoidSafetyEnhanced)
                            throw new Exception(string.Format(
                                "Unable to Create empty object for the class '{0}'.  To eliminate this error " +
                                "a) Add a 'CreateEmpty' method, or " +
                                "b) Create an Empty object separately, or " +
                                "c) Set EnhancedVoidSafety to false.",
                                t.FullName));
                    }
                }
            }
            jClass jc;
            if (isBindNoCheck)
                jc = BindNoCheck(t, emptyWf, ctor, ctorParams);
            else
                jc = Bind(t, emptyWf, ctor, ctorParams);
            if (emptyWf != null)
                Bind(emptyWf);
            jc.CrossCheck(this);
            if (returnClass == null)
                returnClass = jc;
            return returnClass;
        }
        /// <summary>
        /// Creates a 'jClass' and bind the empty jObject.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="empty">This jObject will be made into the empty object for the this 'type'.</param>
        /// <returns></returns>
        public jClass Bind(Type type, jObject empty)
        {
            return this.Bind(type, empty, null, null);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="empty"></param>
        /// <param name="ctor"></param>
        /// <param name="ctorParams"></param>
        /// <returns></returns>
        private jClass Bind(Type type, jObject empty, ConstructorInfo ctor, object[] ctorParams)
        {
            if (typeof(jObject).IsSubclassOf(type))
                throw new ArgumentException(string.Format("The type, '{0}', is not a sub/child class of 'jObject'.  The parameter 'type' needs to inherit from 'jObject'.", type.FullName));
            if (type != empty.GetType())
                throw new ArgumentException(string.Format("The argument 'empty' was expected to be of type '{0}', but was of type '{1}'.", type.FullName, empty.GetType().FullName));
            if (type.IsAbstract)
                throw new ArgumentException(string.Format("The type, '{0}', is abstract.  Abstract class are automatically bound when a child class is bound.", type.FullName));
            return BindNoCheck(type, empty, ctor, ctorParams);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="empty"></param>
        /// <param name="ctor"></param>
        /// <param name="ctorParams"></param>
        /// <returns></returns>
        private jClass BindNoCheck(Type type, jObject empty, ConstructorInfo ctor, object[] ctorParams)
        {
            jClass jC;
            if (Cache.JClasses.TryGet(type, out jC))
            {
                if (jC != jClass.Placeholder)
                {
                    if (!Object.ReferenceEquals(jC.EmptyInstance, empty))
                        throw new ArgumentException(string.Format("The type, '{0}', has already been bound with a empty object '{1}'.", type.FullName, jC.EmptyInstance.FullName));
                    return jC;
                }
            }
            empty.IsEmpty = true;
            jC = new jClass(type, this);
            jC.EmptyInstance = empty;
            if (!this.Cache.Contains(jC.guid))
            {
                this.Cache.Add(jC);
            }
            if (ctor != null)
            {
                ctor.Invoke(empty, ctorParams);
            }
            
            //
            // Bind empty if not bound
            //
            if (!empty.Flags.HasFlag(JazzFlagsType.Bound))
                this.BindObject(empty, type);
            jC.CrossCheck(this);
            #region set IsReadOnly
            empty.Flags = empty.Flags.Set(JazzFlagsType.ReadOnly);
            #endregion

            Type[] classTypes;
            if (type.TryGet_UseEmptyObjectForAttribute(out classTypes))
            {
                foreach (Type classType in classTypes)
                {
                    string name;
                    if (classType.TryGetEmptyObjectAttribute(out name))
                    {
                        throw new JazzConfigurationException(ConfigurationErrorType.RedundantSpecification,
                            string.Format("Redundant Specification: The type '{0}' has the attribute 'EmptyObject' "+
                            "and the type is used in the attribute 'UseEmptyObjectFor' which decorates the type '{0}'." +
                            "Only one of those attributes is allowed.", classType.FullName, type.FullName));
                    }
                    BindNoCheck(classType, empty, null, null);
                }
            }
            //
            // bind base class
            if (jC.JazzObjectType.BaseType != typeof(jObject)
                && jC.JazzObjectType.BaseType != typeof(object)
                && jC.JazzObjectType.BaseType.IsAbstract)
            {
                this.Bind(jC.JazzObjectType.BaseType);
            }
            return jC;
        }
        #endregion
        #region ChangeLoginProfile
        /// <summary>
        /// Changes the login user.  This may result is different behaviour of workflows.
        /// <para>The login roles changing may cause the accessibility of properties and methods
        /// and methods to change.</para>
        /// <para>When the login user is changed then the ACL (Access Control List) of
        /// workflows may change the accessibility of workflows to read/write, read only
        /// or inaccessible.</para>
        /// </summary>
        /// <param name="loginUser"></param>
        public LoggedinUser ChangeLoginProfile(jProfileBase loginUser)
        {
            this._LoginRoles = loginUser._Roles;
            this._LoginProfile = loginUser._Profile;
            this.BindIEnumerable(this._LoginRoles);
            this.Bind(loginUser._User);
            this.BindIEnumerable(loginUser._User._Profiles);
            this.SetObjectAccessibility();
            return new LoggedinUser(loginUser, (ClientNexusBase)this);
        }
        #endregion
        #region SetObjectAccessibility
        /// <summary>
        /// 
        /// </summary>
        private void SetObjectAccessibility()
        {
            this.cache.OfType<jObject>().Foreach(j => j.IsAccessibilitySet = false);
            this.cache.OfType<jObject>().Foreach(j => j.SetAccessibility(this));
        }
        #endregion
        #region IDisposable Members
        /// <summary>
        /// 
        /// </summary>
        public void Dispose()
        {
            //remove connection with nexus
        }

        #endregion
        #region Experimental Code
        /// <summary>
        /// This is an unsupported method.  In future versions it may be removed or cease to operate.
        /// </summary>
        /// <param name="jazzObj"></param>
        protected void UnBind(jObject jazzObj)
        {
            if (!jazzObj.Flags.HasFlag(JazzFlagsType.Initialized))
                throw new ArgumentException("Jazz object needs to be properly initialized.  Bind to nexus first.");
            if (!jazzObj.Flags.HasFlag(JazzFlagsType.Bound))
                throw new ArgumentException("Jazz object is not bound.");
            jazzObj.Flags = jazzObj.Flags.Reset(JazzFlagsType.Bound);
        }
        /// <summary>
        /// This is an unsupported method.  In future versions it may be removed or cease to operate.
        /// </summary>
        /// <param name="jazzObj"></param>
        protected void ReBind(jObject jazzObj)
        {
            if (!jazzObj.Flags.HasFlag(JazzFlagsType.Initialized))
                throw new ArgumentException("Jazz object needs to be properly initialized.  Bind to nexus first.");
            jazzObj.Flags = jazzObj.Flags.Set(JazzFlagsType.Bound);
        }
        #endregion
    }
}
